<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<meta name="keywords" content="spring,Spring,java,web,程序员,博客,IT,技术,移动开发,Web前端,企业架构,编程语言,互联网,数据库,技术博客,blog,分布式架构,分布式">
		<meta name="description" content="cdcoder是一个程序员学习交流平台，深度IT技术博客，移动开发博客,Web前端博客,企业架构博客,编程语言博客,互联网博客,数据库博客,IT blog,核心关注IT知识传播，共享。为更好的IT产业贡献一份微薄的力量。">
		<link rel="shortcut icon" href="/img/code.ico" />
		<link rel="bookmark"href="/img/code.ico" />
		<title>java相关知识</title>
		<link href="/css/index.css" rel="stylesheet">
			<link href="/css/github.css" rel="stylesheet" />
		<style>
			body {
				font-size: 16px;
				font: 400 16px/1.62 Georgia, "Xin Gothic", "Hiragino Sans GB", "Droid Sans Fallback", "Microsoft YaHei", sans-serif;
				font-family: 'PT serif', 微軟正黑體, 微软雅黑, 华文细黑, Microsoft Yahei, Hiragino Sans GB, sans-serif;
				color: #D0CCCC;

				text-shadow: 0px 0px 1px rgba(24, 22, 22, 0.35);
				background-color: #000;
			}
			p{ text-indent:2em; padding:0px; margin:0px; }
			.box ul li { text-indent:4em; padding:0px; margin:0px; }
		</style>
	</head>
	<body>

		<header>
			<div class="logo"><a href="/index.html">徐思润博客</a></div>
			<nav>
				<ul id="starlist">
				<li><a href="/java/index.html">java</a></li>
				<li><a href="/data/index.html">算法与数据结构</a></li>
				<li><a href="/db/mysql.html">数据库</a></li>
				<li><a href="/distribution/index.html">分布式</a></li>
				<li><a href="/framework/index.html">架构</a></li>
				<li><a href="/interview/index.html">面试与被面试</a></li>
				<li><a href="/live/index.html">日常生活</a></li>>
				<li><a href="/about.html">关于我</a></li>
				<li><a href="/baby/index.html">baby墙</a></li>
			</ul>
			</nav>
		</header>
		<div class="box">
			<h2>超时于重试</h2>
<h3>问题的产生</h3>
<p>		在日常的开发中难免遇到调用第三方Api或者调用部分不稳定的接口产生超时或者调用失败的方式，此时我们需要超时和重试机制。</p>
<h3>怎么解决</h3>
<p>		开发过程中解决重试的问题，常见使用 </p>
<p>		循环异常控制去处理此类方法非常简单但是无法设置超时的时间</p>
<p>		好的 我们可以加个延迟队列或者timer去设置超时时间</p>
<p>		使用一段时间后，我们发现需要记录记录一些重试失败的日志和加入一些重试策略</p>
<p>于是我们加入了Listener的去扩展策略和设备记录</p>
<p>		写完一堆代码后我们发现了Guava Retrying 可以完美的替代我们的重试产品</p>
<p>	Maven</p>
<pre><code class='language-java' lang='java'>    &lt;dependency&gt;
      &lt;groupId&gt;com.github.rholder&lt;/groupId&gt;
      &lt;artifactId&gt;guava-retrying&lt;/artifactId&gt;
      &lt;version&gt;2.0.0&lt;/version&gt;
    &lt;/dependency&gt;
</code></pre>
<p>参考实现</p>
<pre><code class='language-java' lang='java'>  //定义重试机制
    Retryer&lt;Boolean&gt; retryer = RetryerBuilder.&lt;Boolean&gt;newBuilder()
            //retryIf 重试条件
            .retryIfException()
            .retryIfRuntimeException()
            .retryIfExceptionOfType(Exception.class)
            .retryIfException(Predicates.equalTo(new Exception()))
            .retryIfResult(Predicates.equalTo(false))

            //等待策略：每次请求间隔1s
            .withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))

            //停止策略 : 尝试请求6次
            .withStopStrategy(StopStrategies.stopAfterAttempt(6))

            //时间限制 : 某次请求不得超过2s , 类似: TimeLimiter timeLimiter = new SimpleTimeLimiter();
            .withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(2, TimeUnit.SECONDS))

            .build();

    //定义请求实现
    Callable&lt;Boolean&gt; callable = new Callable&lt;Boolean&gt;() {
        int times = 1;

        @Override
        public Boolean call() throws Exception {
            log.info(&quot;call times={}&quot;, times);
            times++;

            if (times == 2) {
                throw new NullPointerException();
            } else if (times == 3) {
                throw new Exception();
            } else if (times == 4) {
                throw new RuntimeException();
            } else if (times == 5) {
                return false;
            } else {
                return true;
            }

        }
    };
    //利用重试器调用请求
   return  retryer.call(callable);
</code></pre>
<h1 data-breakpage>guava-retrying实现原理</h1>
<p>guava-retrying的核心是Attempt类、Retryer类以及一些Strategy(策略)相关的类。</p>
<ol>
<li>Attempt</li>

</ol>
<p>Attempt既是一次重试请求(call)，也是请求的结果，并记录了当前请求的次数、是否包含异常和请求的返回值。</p>
<pre><code class='language-java' lang='java'>/**
 * An attempt of a call, which resulted either in a result returned by the call,
 * or in a Throwable thrown by the call.
 *
 * @param &lt;V&gt; The type returned by the wrapped callable.
 * @author JB
 */
public interface Attempt&lt;V&gt;
</code></pre>
<ol>
<li>Retryer</li>

</ol>
<p>Retryer通过RetryerBuilder这个工厂类进行构造。RetryerBuilder负责将定义的重试策略赋值到Retryer对象中。</p>
<p>在Retryer执行call方法的时候，会将这些重试策略一一使用。</p>
<p>下面就看一下Retryer的call方法的具体实现。</p>
<pre><code class='language-java' lang='java'>   public V call(Callable&lt;V&gt; callable) throws ExecutionException, RetryException {
       long startTime = System.nanoTime();
       //说明： 根据attemptNumber进行循环——也就是重试多少次
       for (int attemptNumber = 1; ; attemptNumber++) {
           //说明：进入方法不等待，立即执行一次
           Attempt&lt;V&gt; attempt;
           try {
                //说明：执行callable中的具体业务
                //attemptTimeLimiter限制了每次尝试等待的时常
               V result = attemptTimeLimiter.call(callable);
               //利用调用结果构造新的attempt
               attempt = new ResultAttempt&lt;V&gt;(result, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
           } catch (Throwable t) {
               attempt = new ExceptionAttempt&lt;V&gt;(t, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
           }

           //说明：遍历自定义的监听器
           for (RetryListener listener : listeners) {
               listener.onRetry(attempt);
           }

           //说明：判断是否满足重试条件，来决定是否继续等待并进行重试
           if (!rejectionPredicate.apply(attempt)) {
               return attempt.get();
           }

           //说明：此时满足停止策略，因为还没有得到想要的结果，因此抛出异常
           if (stopStrategy.shouldStop(attempt)) {
               throw new RetryException(attemptNumber, attempt);
           } else {
                //说明：执行默认的停止策略——线程休眠
               long sleepTime = waitStrategy.computeSleepTime(attempt);
               try {
                   //说明：也可以执行定义的停止策略
                   blockStrategy.block(sleepTime);
               } catch (InterruptedException e) {
                   Thread.currentThread().interrupt();
                   throw new RetryException(attemptNumber, attempt);
               }
           }
       }
   }
</code></pre>
<p>&nbsp;</p>
<h1 data-breakpage>guava-retrying高级用法</h1>
<p>基于guava-retrying的实现原理，我们可以根据实际业务来确定自己的重试策略。</p>
<p>下面以<code>同步数据</code>这种常规系统业务为例，自定义重试策略。</p>
<p>如下实现基于Spring Boot 2.1.2.RELEASE版本。</p>
<p>并使用Lombok简化Bean。</p>
<pre><code>&lt;dependency&gt;
     &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;
     &lt;artifactId&gt;lombok&lt;/artifactId&gt;
     &lt;optional&gt;true&lt;/optional&gt;
&lt;/dependency&gt;
</code></pre>
<h2>业务描述</h2>
<p>当商品创建以后，需要另外设置商品的价格。由于两个操作是有两个人进行的，因此会出现如下问题，即商品没有创建，但是价格数据却已经建好了。遇到这种情况，价格数据需要等待商品正常创建以后，继续完成同步。</p>
<p>我们通过一个http请求进行商品的创建，同时通过一个定时器来修改商品的价格。</p>
<p>当商品不存在，或者商品的数量小于1的时候，商品的价格不能设置。需要等商品成功创建且数量大于0的时候，才能将商品的价格设置成功。</p>
<h2>实现过程</h2>
<ol>
<li>自定义重试阻塞策略</li>

</ol>
<p>默认的阻塞策略是线程休眠，这里使用自旋锁实现，不阻塞线程。</p>
<pre><code class='language-java' lang='java'>package net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.strategy;

import com.github.rholder.retry.BlockStrategy;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.time.Duration;
import java.time.LocalDateTime;

/**
 * 自旋锁的实现, 不响应线程中断
 */
@Slf4j
@NoArgsConstructor
public class SpinBlockStrategy implements BlockStrategy {

    @Override
    public void block(long sleepTime) throws InterruptedException {

        LocalDateTime startTime = LocalDateTime.now();

        long start = System.currentTimeMillis();
        long end = start;
        log.info(&quot;[SpinBlockStrategy]...begin wait.&quot;);

        while (end - start &lt;= sleepTime) {
            end = System.currentTimeMillis();
        }

        //使用Java8新增的Duration计算时间间隔
        Duration duration = Duration.between(startTime, LocalDateTime.now());

        log.info(&quot;[SpinBlockStrategy]...end wait.duration={}&quot;, duration.toMillis());

    }
}
</code></pre>
<ol>
<li>自定义重试监听器</li>

</ol>
<p>RetryListener可以监控多次重试过程，并可以使用<code>attempt</code>做一些额外的事情。</p>
<pre><code class='language-java' lang='java'>package net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.listener;

import com.github.rholder.retry.Attempt;
import com.github.rholder.retry.RetryListener;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class RetryLogListener implements RetryListener {

    @Override
    public &lt;V&gt; void onRetry(Attempt&lt;V&gt; attempt) {

        // 第几次重试,(注意:第一次重试其实是第一次调用)
        log.info(&quot;retry time : [{}]&quot;, attempt.getAttemptNumber());

        // 距离第一次重试的延迟
        log.info(&quot;retry delay : [{}]&quot;, attempt.getDelaySinceFirstAttempt());

        // 重试结果: 是异常终止, 还是正常返回
        log.info(&quot;hasException={}&quot;, attempt.hasException());
        log.info(&quot;hasResult={}&quot;, attempt.hasResult());

        // 是什么原因导致异常
        if (attempt.hasException()) {
            log.info(&quot;causeBy={}&quot; , attempt.getExceptionCause().toString());
        } else {
            // 正常返回时的结果
            log.info(&quot;result={}&quot; , attempt.getResult());
        }

        log.info(&quot;log listen over.&quot;);

    }
}
</code></pre>
<ol>
<li>自定义Exception</li>

</ol>
<p>有些异常需要重试，有些不需要。</p>
<pre><code class='language-java' lang='java'>package net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.exception;

/**
 * 当抛出这个异常的时候，表示需要重试
 */
public class NeedRetryException extends Exception {

    public NeedRetryException(String message) {
        super(&quot;NeedRetryException can retry.&quot;+message);
    }

}
</code></pre>
<ol>
<li>实现具体重试业务与Callable接口</li>

</ol>
<p>使用call方法调用自己的业务。</p>
<pre><code class='language-java' lang='java'>package net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.model;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.math.BigDecimal;

/**
 * 商品model
 */
@Data
@AllArgsConstructor
public class Product {

    private Long id;

    private String name;

    private Integer count;

    private BigDecimal price;

}
package net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.repository;

import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.model.Product;
import org.springframework.stereotype.Repository;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 商品DAO
 */
@Repository
public class ProductRepository {

    private static ConcurrentHashMap&lt;Long,Product&gt; products=new  ConcurrentHashMap();

    private static AtomicLong ids=new AtomicLong(0);

    public List&lt;Product&gt; findAll(){
        return new ArrayList&lt;&gt;(products.values());
    }

    public Product findById(Long id){
        return products.get(id);
    }

    public Product updatePrice(Long id, BigDecimal price){
        Product p=products.get(id);
        if (null==p){
            return p;
        }
        p.setPrice(price);
        return p;
    }

    public Product addProduct(Product product){
        Long id=ids.addAndGet(1);
        product.setId(id);
        products.put(id,product);
        return product;
    }

}
package net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.service;

import lombok.extern.slf4j.Slf4j;
import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.exception.NeedRetryException;
import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.model.Product;
import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;

/**
 * 业务方法实现
 */
@Component
@Slf4j
public class ProductInformationHander implements Callable&lt;Boolean&gt; {

    @Autowired
    private ProductRepository pRepo;

    private static Map&lt;Long, BigDecimal&gt; prices = new HashMap&lt;&gt;();

    static {
        prices.put(1L, new BigDecimal(100));
        prices.put(2L, new BigDecimal(200));
        prices.put(3L, new BigDecimal(300));
        prices.put(4L, new BigDecimal(400));
        prices.put(8L, new BigDecimal(800));
        prices.put(9L, new BigDecimal(900));
    }

    @Override
    public Boolean call() throws Exception {

        log.info(&quot;sync price begin,prices size={}&quot;, prices.size());

        for (Long id : prices.keySet()) {
            Product product = pRepo.findById(id);

            if (null == product) {
                throw new NeedRetryException(&quot;can not find product by id=&quot; + id);
            }
            if (null == product.getCount() || product.getCount() &lt; 1) {
                throw new NeedRetryException(&quot;product count is less than 1, id=&quot; + id);
            }

            Product updatedP = pRepo.updatePrice(id, prices.get(id));
            if (null == updatedP) {
                return false;
            }

            prices.remove(id);
        }

        log.info(&quot;sync price over,prices size={}&quot;, prices.size());

        return true;
    }

}
</code></pre>
<ol>
<li>构造重试器Retryer</li>

</ol>
<p>将上面的实现作为参数，构造Retryer。</p>
<pre><code class='language-java' lang='java'>package net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.service;

import com.github.rholder.retry.*;
import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.exception.NeedRetryException;
import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.listener.RetryLogListener;
import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.strategy.SpinBlockStrategy;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * 构造重试器
 */
@Component
public class ProductRetryerBuilder {

    public Retryer build() {
        //定义重试机制
        Retryer&lt;Boolean&gt; retryer = RetryerBuilder.&lt;Boolean&gt;newBuilder()

                //retryIf 重试条件
                //.retryIfException()
                //.retryIfRuntimeException()
                //.retryIfExceptionOfType(Exception.class)
                //.retryIfException(Predicates.equalTo(new Exception()))
                //.retryIfResult(Predicates.equalTo(false))
                .retryIfExceptionOfType(NeedRetryException.class)

                //等待策略：每次请求间隔1s
                .withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))

                                //停止策略 : 尝试请求3次
                .withStopStrategy(StopStrategies.stopAfterAttempt(3))

                //时间限制 : 某次请求不得超过2s , 类似: TimeLimiter timeLimiter = new SimpleTimeLimiter();
                .withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(2, TimeUnit.SECONDS))

                //默认的阻塞策略：线程睡眠
                //.withBlockStrategy(BlockStrategies.threadSleepStrategy())
                //自定义阻塞策略：自旋锁
                .withBlockStrategy(new SpinBlockStrategy())

                //自定义重试监听器
                .withRetryListener(new RetryLogListener())

                .build();

        return retryer;

    }
}
</code></pre>
<ol>
<li>与定时任务结合执行Retryer</li>

</ol>
<p>定时任务只需要跑一次，但是实际上实现了所有的重试策略。这样大大简化了定时器的设计。</p>
<p>首先使用<code>@EnableScheduling</code>声明项目支持定时器注解。</p>
<pre><code class='language-java' lang='java'>@SpringBootApplication
@EnableScheduling
public class DemoRetryerApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoRetryerApplication.class, args);
    }
}
package net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.task;

import com.github.rholder.retry.Retryer;
import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.service.ProductInformationHander;
import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.service.ProductRetryerBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
* 商品信息定时器
*/
@Component
public class ProductScheduledTasks {

    @Autowired
    private ProductRetryerBuilder builder;

    @Autowired
    private ProductInformationHander hander;

    /**
     * 同步商品价格定时任务
     * @Scheduled(fixedDelay = 30000) ：上一次执行完毕时间点之后30秒再执行
     */
    @Scheduled(fixedDelay = 30*1000)
    public void syncPrice() throws Exception{
        Retryer retryer=builder.build();
        retryer.call(hander);
    }

}
</code></pre>
<p>执行结果：由于并没有商品，因此重试以后，抛出异常。</p>
<pre><code class='language-java' lang='java'>2019-二月-28 14:37:52.667 INFO  [scheduling-1] n.i.t.f.s.i.d.r.g.l.RetryLogListener - log listen over.
2019-二月-28 14:37:52.672 ERROR [scheduling-1] o.s.s.s.TaskUtils$LoggingErrorHandler - Unexpected error occurred in scheduled task.
com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 3 attempts.
    at com.github.rholder.retry.Retryer.call(Retryer.java:174)
</code></pre>
<p> </p>
<p>linkes</p>
参考：
<p>	<a href='https://zhuanlan.zhihu.com/p/57970758' target='_blank' class='url'>https://zhuanlan.zhihu.com/p/57970758</a></p>

		 </div>
		<footer>
			<p>Design by <a href="http://cdcoder.cn" target="_blank">徐思润个人博客</a> </p>
			<p>备案号：<a href="/">蜀ICP备15014105号-1</a></p>
			<script type="text/javascript" src="https://s96.cnzz.com/z_stat.php?id=1277229191&web_id=1277229191"></script>
		</footer>
	<script src="/js/rainbow.min.js"></script>
		<script src="/js/java_h.js"></script>
	</body>
</html>
